<!--
  文件级注释：图形化分账树组件（SVG 连线 + 卡片节点）
  - 以树结构数据渲染为参考图形式的组织图
  - 内部包含简单的布局算法（按叶子数分配水平空间、逐层排列）
  - 使用 SVG 绘制父子连线，支持曲线及虚线
  使用规则：传入统一树结构 [{ id,label,ratio,amount,type,children[], omitted? }]
-->

<template>
  <div
    class="divided-tree"
    ref="container"
    :style="containerStyle"
    :class="{ grabbing: isDragging }"
    @wheel.prevent="onWheel"
    @mousedown="onMouseDown"
    @mousemove="onMouseMove"
    @mouseup="onMouseUp"
    @mouseleave="onMouseUp"
  >
    <!-- 工具栏：缩放与视图控制 -->
    <div class="toolbox">
      <button class="tool-btn" @click="zoomOut">-</button>
      <button class="tool-btn" @click="zoomIn">+</button>
      <button class="tool-btn" @click="resetView">重置</button>
      <button class="tool-btn" @click="fitToView">适配</button>
    </div>

    <!-- 画布：整体缩放/平移应用在此容器 -->
    <div class="canvas" :style="canvasStyle">
      <!-- SVG 连线层 -->
      <svg class="links" :width="layout.width" :height="layout.height">
        <path
          v-for="(ln, i) in layout.links"
          :key="i"
          :d="bezierPath(ln)"
          :class="['link', ln.dashed ? 'link-dashed' : '']"
        />
      </svg>

      <!-- 卡片节点层（绝对定位） -->
      <div
        v-for="node in layout.nodes"
        :key="node.key"
        class="node-card"
        :style="{ left: node.x + 'px', top: node.y + 'px', width: nodeW + 'px', height: nodeH + 'px' }"
      >
        <div class="node-title">
          <span :class="['icon', node.type === 'agreement' ? 'icon-agreement' : 'icon-user']"></span>
          <span class="name">{{ node.label || '-' }}</span>
        </div>
        <div class="node-row">分账比例：{{ node.ratio ?? '-' }}</div>
        <div class="node-row">分账金额：{{ node.amount ?? '-' }}</div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
/**
 * 文件级注释：DividedTree 组件
 * 功能：以卡片+曲线连线展示分账树，并支持缩放(滚轮/按钮)、拖拽平移、视图适配居中。
 * 参数：
 *  - data：归一化树数据数组（根节点列表）
 *  - nodeWidth：节点卡片宽度（可选）
 *  - nodeHeight：节点卡片高度（可选）
 *  - hGap：同层子节点水平间距（可选）
 *  - vGap：层级垂直间距（可选）
 *  - viewportHeight：视口固定高度，便于大图浏览（可选）
 * 返回值：Vue 组件渲染内容，无直接返回值
 * 异常：无显式异常；若容器未挂载，适配函数会提前返回
 */
import { computed, ref, watch, nextTick, onMounted } from 'vue'

/** 类级注释：图形化分账树组件（支持协议/参与人两类节点与虚线占位） */

/**
 * 参数注释：组件入参
 * @prop data 统一树结构根数组
 * @prop nodeWidth 节点卡片宽度（默认180）
 * @prop nodeHeight 节点卡片高度（默认88）
 * @prop hGap 水平间距（默认40）
 * @prop vGap 垂直间距（默认60）
 */
/**
 * 组件级注释：DividedTree
 * 说明：接收归一化树数据，内部计算布局，并提供交互控制以优化大规模树的浏览体验。
 */
const props = defineProps<{
  data: any[]
  nodeWidth?: number
  nodeHeight?: number
  hGap?: number
  vGap?: number
  viewportHeight?: number
}>()

/** 返回值注释：无显式返回值（渲染型组件） */

/** 内部尺寸与容器引用 */
const nodeW = props.nodeWidth ?? 180
const nodeH = props.nodeHeight ?? 88
const hGap = props.hGap ?? 40
const vGap = props.vGap ?? 60
const container = ref<HTMLDivElement | null>(null)
const viewportH = props.viewportHeight ?? 600

/** 视图变换状态：缩放与位移 */
const scale = ref<number>(1)
const offsetX = ref<number>(0)
const offsetY = ref<number>(0)
const isDragging = ref<boolean>(false)
const dragStart = ref<{x:number;y:number}|null>(null)

/**
 * 函数级注释：计算叶子数（用于水平空间分配）
 * @param n 节点对象（含 children 数组）
 * @returns number 该节点子树的叶子数量；无子节点时为1
 * 异常：无显式异常
 */
function leafCount(n: any): number {
  if (!n || !Array.isArray(n.children) || n.children.length === 0) return 1
  return n.children.map(leafCount).reduce((a: number, b: number) => a + b, 0)
}

/**
 * 函数级注释：生成布局（节点坐标与连线）
 * @param roots 根节点数组
 * @returns { width,height,nodes,links } 用于渲染的布局数据
 * 异常：无显式异常（空数组返回最小画布）
 */
function buildLayout(roots: any[]): { width: number; height: number; nodes: any[]; links: any[] } {
  const placed: any[] = []
  const lines: any[] = []

  // 列宽以“叶子单元”为单位分配
  const unitW = nodeW + hGap
  const unitH = nodeH + vGap

  // 计算每个根的叶子数，确定总宽度
  const rootLeafs = roots.map(r => leafCount(r))
  const totalLeafs = rootLeafs.reduce((a, b) => a + b, 0)

  // 递归安放节点：先放子，再放父（父居中于其子树跨度）
  const placeNode = (n: any, depth: number, startUnit: number, pathKey: string): { centerUnit: number } => {
    let unitOffset = startUnit
    const childCenters: number[] = []

    if (Array.isArray(n.children) && n.children.length > 0) {
      for (let i = 0; i < n.children.length; i++) {
        const c = n.children[i]
        const cLeafs = leafCount(c)
        const childPath = `${pathKey}/${i}`
        const childPlaced = placeNode(c, depth + 1, unitOffset, childPath)
        childCenters.push(childPlaced.centerUnit)
        // 记录父子连线
        lines.push({
          x1: childPlaced.centerUnit * unitW + nodeW / 2,
          y1: (depth + 1) * unitH + nodeH / 2,
          x0: undefined,
          y0: undefined,
          x2: undefined,
          y2: undefined,
          fromDepth: depth,
          toDepth: depth + 1,
          dashed: false,
          // 父坐标待父节点确定后再修正（此处先记录子端）
          childKey: `${pathKey}-${i}`
        })
        unitOffset += cLeafs
      }
    } else {
      // 若声明有子协议但未返回子层，补一个虚线占位连线
      if (n.omitted === true) {
        const cx = (startUnit + 0.5) * unitW + nodeW / 2
        const cy = depth * unitH + nodeH
        lines.push({ x1: cx, y1: cy + 8, dashed: true, fromDepth: depth, toDepth: depth + 1, childKey: `${pathKey}-omitted` })
      }
    }

    const spanLeafs = leafCount(n)
    const centerUnit = startUnit + spanLeafs / 2
    const x = centerUnit * unitW - nodeW / 2
    const y = depth * unitH
    const key = `${n.id || 'node'}@${pathKey}`

    placed.push({ key, x, y, label: n.label, ratio: n.ratio, amount: n.amount, type: n.type || 'node' })

    // 将父端坐标补齐到连线中（以同一深度的记录匹配）
    for (const ln of lines) {
      if (ln.fromDepth === depth && ln.x0 === undefined) {
        ln.x0 = centerUnit * unitW + nodeW / 2
        ln.y0 = y + nodeH / 2
      }
    }

    return { centerUnit }
  }

  let start = 0
  for (let r = 0; r < roots.length; r++) {
    const root = roots[r]
    const leafs = rootLeafs[r]
    placeNode(root, 0, start, `r${r}`)
    start += leafs
  }

  const maxDepth = Math.max(0, ...placed.map(p => Math.round(p.y / unitH)))
  const width = Math.max(600, totalLeafs * unitW)
  const height = Math.max(300, (maxDepth + 1) * unitH + nodeH)

  // 生成最终连线坐标（父子曲线）
  const links = lines
    .filter(ln => ln.x0 !== undefined && ln.y0 !== undefined && ln.x1 !== undefined && ln.y1 !== undefined)
    .map(ln => ({ x1: ln.x0, y1: ln.y0, x2: ln.x1, y2: ln.y1, dashed: ln.dashed }))

  return { width, height, nodes: placed, links }
}

/**
 * 函数级注释：生成贝塞尔曲线路径
 * @param ln 连线对象（包含起终点）
 * @returns string SVG path d 属性字符串
 * 异常：无显式异常
 */
function bezierPath(ln: { x1: number; y1: number; x2: number; y2: number }): string {
  const xm = (ln.x1 + ln.x2) / 2
  const ym = (ln.y1 + ln.y2) / 2
  return `M ${ln.x1} ${ln.y1} C ${ln.x1} ${ym}, ${ln.x2} ${ym}, ${ln.x2} ${ln.y2}`
}

/** 布局计算：将传入树转换为坐标与连线 */
const layout = computed(() => {
  const roots = Array.isArray(props.data) ? props.data : []
  return buildLayout(roots)
})

/**
 * 函数级注释：容器样式（绑定布局宽高，确保绝对定位内容可见）
 * @returns 样式对象，包含 minWidth 与 minHeight；同时设置固定 height 以撑开容器
 * 异常：无显式异常
 */
const containerStyle = computed(() => ({
  width: '100%',
  height: viewportH + 'px',
  minHeight: viewportH + 'px'
}))

/**
 * 函数级注释：画布样式（应用缩放与平移）
 * @returns 样式对象，包含宽高、transform 与 transformOrigin
 * 异常：无显式异常
 */
const canvasStyle = computed(() => ({
  width: layout.value.width + 'px',
  height: layout.value.height + 'px',
  transform: `translate(${offsetX.value}px, ${offsetY.value}px) scale(${scale.value})`,
  transformOrigin: '0 0'
}))

/**
 * 函数级注释：限制数值范围
 * @param v 当前值
 * @param min 最小值
 * @param max 最大值
 * @returns number 夹取后的值
 */
function clamp(v: number, min: number, max: number): number {
  return Math.max(min, Math.min(max, v))
}

/**
 * 函数级注释：缩放视图（放大）
 * @returns void
 */
function zoomIn(): void {
  const newScale = clamp(scale.value * 1.1, 0.2, 3)
  scale.value = newScale
}

/**
 * 函数级注释：缩放视图（缩小）
 * @returns void
 */
function zoomOut(): void {
  const newScale = clamp(scale.value * 0.9, 0.2, 3)
  scale.value = newScale
}

/**
 * 函数级注释：重置视图到初始状态
 * @returns void
 */
function resetView(): void {
  scale.value = 1
  offsetX.value = 0
  offsetY.value = 0
}

/**
 * 函数级注释：根据容器尺寸自动适配并居中内容
 * @returns void
 * @throws 可能因容器未挂载导致读取尺寸为0
 */
function fitToView(): void {
  const el = container.value
  if (!el) return
  const cw = el.clientWidth
  const ch = el.clientHeight
  const wScale = cw / (layout.value.width || 1)
  const hScale = ch / (layout.value.height || 1)
  const s = clamp(Math.min(wScale, hScale) * 0.95, 0.2, 3)
  scale.value = s
  // 居中内容
  offsetX.value = (cw - layout.value.width * s) / 2
  offsetY.value = (ch - layout.value.height * s) / 2
}

/**
 * 函数级注释：滚轮缩放，保持光标位置不跳变
 * @param e WheelEvent
 * @returns void
 */
function onWheel(e: WheelEvent): void {
  const el = container.value
  if (!el) return
  const rect = el.getBoundingClientRect()
  const cursorX = e.clientX - rect.left
  const cursorY = e.clientY - rect.top
  const scaleFactor = e.deltaY > 0 ? 0.9 : 1.1
  const newScale = clamp(scale.value * scaleFactor, 0.2, 3)

  // 以光标为参考进行缩放，修正偏移量
  const dx = (cursorX - offsetX.value) / scale.value
  const dy = (cursorY - offsetY.value) / scale.value
  offsetX.value = cursorX - dx * newScale
  offsetY.value = cursorY - dy * newScale
  scale.value = newScale
}

/**
 * 函数级注释：开始拖拽平移
 * @param e MouseEvent
 * @returns void
 */
function onMouseDown(e: MouseEvent): void {
  isDragging.value = true
  dragStart.value = { x: e.clientX, y: e.clientY }
}

/**
 * 函数级注释：拖拽过程更新偏移量
 * @param e MouseEvent
 * @returns void
 */
function onMouseMove(e: MouseEvent): void {
  if (!isDragging.value || !dragStart.value) return
  const dx = e.clientX - dragStart.value.x
  const dy = e.clientY - dragStart.value.y
  dragStart.value = { x: e.clientX, y: e.clientY }
  offsetX.value += dx
  offsetY.value += dy
}

/**
 * 函数级注释：结束拖拽
 * @returns void
 */
function onMouseUp(): void {
  isDragging.value = false
  dragStart.value = null
}

onMounted(() => {
  // 初次渲染后适配视图
  nextTick(() => fitToView())
})

watch(() => [layout.value.width, layout.value.height], () => {
  // 布局变更（数据刷新）后再次适配
  nextTick(() => fitToView())
})
</script>

<style scoped>
.divided-tree { position: relative; width: 100%; overflow: hidden; background: #f6f7f9; border: 1px solid var(--border); border-radius: 8px; cursor: grab; }
.divided-tree.grabbing { cursor: grabbing; }
.canvas { position: absolute; left: 0; top: 0; }
.links { position: absolute; left: 0; top: 0; }
.link { fill: none; stroke: #333; stroke-width: 1.6; }
.link-dashed { stroke-dasharray: 4 4; stroke: #888; }

.node-card { position: absolute; background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; box-shadow: 0 1px 2px rgba(0,0,0,0.06); padding: 10px 12px; display: grid; gap: 6px; }
.node-title { display: flex; align-items: center; gap: 8px; font-weight: 600; }
.node-row { font-size: 12px; color: #666; }
.icon { width: 16px; height: 16px; display: inline-block; border-radius: 4px; }
.icon-agreement { background: #4a90e2; }
.icon-user { background: #7ed321; }

.toolbox { position: absolute; right: 12px; top: 12px; display: flex; gap: 8px; z-index: 10; }
.tool-btn { height: 28px; padding: 0 10px; border: 1px solid #d1d5db; background: #fff; border-radius: 6px; cursor: pointer; }
</style>
